﻿namespace Neovolve.Extensibility.ReSharper
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Text.RegularExpressions;
    using JetBrains.ReSharper.Psi;
    using JetBrains.ReSharper.Psi.CSharp.Parsing;
    using JetBrains.ReSharper.Psi.CSharp.Tree;
    using JetBrains.ReSharper.Psi.Impl.PsiManagerImpl;
    using JetBrains.ReSharper.Psi.Tree;

    /// <summary>
    /// The <see cref="NodeAnalyser"/>
    ///   class is used to analyse <see cref="ITreeNode"/> instances to determine how they should be processed.
    /// </summary>
    internal static class NodeAnalyser
    {
        /// <summary>
        /// Defines the minimum index value that can identify a cref value in a comment.
        /// </summary>
        public const Int32 MinimumCommentCaretIndex = 15;

        /// <summary>
        /// Defines the regular expression value for extracting cref values from a comment.
        /// </summary>
        private const String CrefExpressionValue = "\\s+cref\\s*=\\s*\"[^\"]*(?<typeName>\\b\\w+\\b)[^\"]*\"";

        /// <summary>
        /// Stores the regular expression for extracting cref values from a comment.
        /// </summary>
        private static readonly Regex _crefExpression = new Regex(
            CrefExpressionValue, RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);

        /// <summary>
        /// Stores the type mappings of alias type declarations.
        /// </summary>
        private static IEnumerable<AliasTypeMapping> _aliasTypeMappings;

        /// <summary>
        /// Gets the bulb item.
        /// </summary>
        /// <param name="commentNode">
        /// The comment node.
        /// </param>
        /// <param name="commentIndex">
        /// Index of the comment.
        /// </param>
        /// <returns>
        /// An <see cref="IConvertTypeBulbItem"/> value or <c>null</c> if no item is found for the node.
        /// </returns>
        public static NodeDescriptor GetNodeDescriptor(IDocCommentNode commentNode, Int32 commentIndex)
        {
            CommentNodeSetDescriptor descriptorSet = GetCommentDescriptor(commentNode, commentIndex);

            if (descriptorSet == null)
            {
                return null;
            }

            if (descriptorSet.IsValid == false)
            {
                return null;
            }

            return descriptorSet;
        }

        /// <summary>
        /// Gets the node descriptor.
        /// </summary>
        /// <param name="tokenNode">
        /// The token node.
        /// </param>
        /// <returns>
        /// A <see cref="NodeDescriptor"/> instance or <c>null</c> if no valid <see cref="NodeDescriptor"/> is available.
        /// </returns>
        public static NodeDescriptor GetNodeDescriptor(ITreeNode tokenNode)
        {
            if (tokenNode == null)
            {
                return null;
            }

            if (tokenNode is IDocCommentNode)
            {
                throw new ArgumentException(
                    "This method does not support IDocCommentNode. Invoke GetNodeDescriptor(IDocCommentNode, Int32) for this type of node.");
            }

            NodeDescriptor descriptor = GetDescriptorForNode(tokenNode);

            if (descriptor == null)
            {
                return null;
            }

            if (descriptor.IsValid == false)
            {
                return null;
            }

            return descriptor;
        }

        /// <summary>
        /// Resolves the formattable nodes.
        /// </summary>
        /// <param name="node">
        /// The node to process.
        /// </param>
        /// <param name="rangeMarker">
        /// The range marker.
        /// </param>
        /// <param name="psiManager">
        /// The psi manager.
        /// </param>
        /// <returns>
        /// A <see cref="Collection{T}"/> instance.
        /// </returns>
        public static IEnumerable<NodeDescriptor> ResolveNodeDescriptors(ITreeNode node, IPsiRangeMarker rangeMarker, PsiManagerImpl psiManager)
        {
            Collection<NodeDescriptor> nodes = new Collection<NodeDescriptor>();

            // Resolve the nodes
            ResolveNodeDescriptorsInternal(node, rangeMarker, psiManager, nodes);

            return nodes;
        }

        /// <summary>
        /// Generates the mappings.
        /// </summary>
        /// <returns>
        /// A <see cref="IEnumerable{T}"/> instance.
        /// </returns>
        private static IEnumerable<AliasTypeMapping> GenerateMappings()
        {
            IEnumerable<AliasTypeMapping> mappings = new List<AliasTypeMapping>
                                                     {
                                                         new AliasTypeMapping("object", CSharpTokenType.OBJECT_KEYWORD, "Object"), 
                                                         new AliasTypeMapping("char", CSharpTokenType.CHAR_KEYWORD, "Char"), 
                                                         new AliasTypeMapping("string", CSharpTokenType.STRING_KEYWORD, "String"), 
                                                         new AliasTypeMapping("bool", CSharpTokenType.BOOL_KEYWORD, "Boolean"), 
                                                         new AliasTypeMapping("byte", CSharpTokenType.BYTE_KEYWORD, "Byte"), 
                                                         new AliasTypeMapping("short", CSharpTokenType.SHORT_KEYWORD, "Int16"), 
                                                         new AliasTypeMapping("int", CSharpTokenType.INT_KEYWORD, "Int32"), 
                                                         new AliasTypeMapping("long", CSharpTokenType.LONG_KEYWORD, "Int64"), 
                                                         new AliasTypeMapping("ushort", CSharpTokenType.USHORT_KEYWORD, "UInt16"), 
                                                         new AliasTypeMapping("uint", CSharpTokenType.UINT_KEYWORD, "UInt32"), 
                                                         new AliasTypeMapping("ulong", CSharpTokenType.ULONG_KEYWORD, "UInt64"), 
                                                         new AliasTypeMapping("double", CSharpTokenType.DOUBLE_KEYWORD, "Double"), 
                                                         new AliasTypeMapping("decimal", CSharpTokenType.DECIMAL_KEYWORD, "Decimal"), 
                                                         new AliasTypeMapping("float", CSharpTokenType.FLOAT_KEYWORD, "Single"), 
                                                         new AliasTypeMapping("sbyte", CSharpTokenType.SBYTE_KEYWORD, "SByte")
                                                     };

            return mappings;
        }

        /// <summary>
        /// Gets the comment cref matches.
        /// </summary>
        /// <param name="commentNode">
        /// The comment node.
        /// </param>
        /// <returns>
        /// A <see cref="MatchCollection"/> instance.
        /// </returns>
        private static MatchCollection GetCommentCrefMatches(IDocCommentNode commentNode)
        {
            String nodeText = commentNode.GetText();

            return _crefExpression.Matches(nodeText);
        }

        /// <summary>
        /// Gets the comment descriptor.
        /// </summary>
        /// <param name="node">
        /// The node to describe.
        /// </param>
        /// <param name="commentIndex">
        /// Index of the comment.
        /// </param>
        /// <returns>
        /// A <see cref="CommentNodeSetDescriptor"/> value.
        /// </returns>
        private static CommentNodeSetDescriptor GetCommentDescriptor(ITreeNode node, Int32 commentIndex)
        {
            IDocCommentNode commentNode = node as IDocCommentNode;

            if (commentNode == null)
            {
                return null;
            }

            if (commentNode.CommentType != CommentType.DOC_COMMENT)
            {
                return null;
            }

            // We can ignore the leading /// <see cref= before it is possible to get a cref type reference
            if (commentIndex > -1 && commentIndex < MinimumCommentCaretIndex)
            {
                return null;
            }

            CommentNodeSetDescriptor descriptorSet = null;
            MatchCollection crefMatches = GetCommentCrefMatches(commentNode);
            List<CommentNodeDescriptor> descriptors = new List<CommentNodeDescriptor>();

            foreach (Match match in crefMatches)
            {
                Group group = match.Groups["typeName"];

                if (commentIndex > -1)
                {
                    // Check if this index is in the match group
                    if (group.Index > commentIndex || group.Index + group.Length < commentIndex)
                    {
                        continue;
                    }
                }

                String typeName = group.Value;
                CommentNodeDescriptor descriptor = new CommentNodeDescriptor(commentNode, group.Index, typeName);

                if (descriptor.IsValid)
                {
                    descriptors.Add(descriptor);
                }

                if (commentIndex > -1)
                {
                    if (group.Index + group.Length > commentIndex)
                    {
                        break;
                    }
                }
            }

            if (descriptors.Count > 0)
            {
                descriptorSet = new CommentNodeSetDescriptor(commentNode, descriptors);
            }

            return descriptorSet;
        }

        /// <summary>
        /// Gets the descriptor for node.
        /// </summary>
        /// <param name="node">
        /// The node.
        /// </param>
        /// <returns>
        /// A <see cref="NodeDescriptor"/> value.
        /// </returns>
        private static NodeDescriptor GetDescriptorForNode(ITreeNode node)
        {
            if (node is IPredefinedTypeReferenceNode)
            {
                return new PredefinedTypeReferenceNodeDescriptor(node);
            }

            if (node is IPredefinedTypeExpressionNode)
            {
                return new PredefinedTypeExpressionNodeDescriptor(node);
            }

            if (node is IReferenceExpressionNode)
            {
                return new ReferenceExpressionNodeDescriptor(node);
            }

            if (node is IDeclaredTypeUsageNode)
            {
                return new DeclaredTypeUsageNodeDescriptor(node);
            }

            // NOTE: This needs to be evaluated before ITypeUsageNode as IUserTypeUsageNode is a derived type
            if (node is IUserTypeUsageNode)
            {
                return new UserTypeUsageNodeDescriptor(node);
            }

            if (node is IPredefinedTypeUsageNode)
            {
                return new PredefinedTypeUsageNodeDescriptor(node);
            }

            if (node is ITypeUsageNode)
            {
                return new TypeUsageNodeDescriptor(node);
            }

            if (node is IDocCommentNode)
            {
                return GetCommentDescriptor(node, -1);
            }

            if (node is ITokenNode)
            {
                return RecurseParents(node, 3);
            }

            return null;
        }

        /// <summary>
        /// Recurses the parents.
        /// </summary>
        /// <param name="node">
        /// The node.
        /// </param>
        /// <param name="parentChecksRemaining">
        /// The parent checks remaining.
        /// </param>
        /// <returns>
        /// A <see cref="NodeDescriptor"/> instance or <c>null</c> if no valid <see cref="NodeDescriptor"/> can be found.
        /// </returns>
        private static NodeDescriptor RecurseParents(ITreeNode node, Int32 parentChecksRemaining)
        {
            if (parentChecksRemaining > 0 && node.Parent != null)
            {
                NodeDescriptor descriptor = RecurseParents(node.Parent, parentChecksRemaining - 1);

                if (descriptor != null)
                {
                    return descriptor;
                }
            }

            // Need to protect against token nodes as this would cause a stack overflow on GetDescriptorForNode
            if (node is ITokenNode)
            {
                return null;
            }

            return GetDescriptorForNode(node);
        }

        /// <summary>
        /// Resolves the formatable nodes internal.
        /// </summary>
        /// <param name="node">
        /// The node to process.
        /// </param>
        /// <param name="rangeMarker">
        /// The range marker.
        /// </param>
        /// <param name="psiManager">
        /// The psi manager.
        /// </param>
        /// <param name="results">
        /// The results.
        /// </param>
        /// <returns>
        /// A <see cref="ITreeNode"/> instance.
        /// </returns>
        private static ITreeNode ResolveNodeDescriptorsInternal(
            ITreeNode node, IPsiRangeMarker rangeMarker, PsiManagerImpl psiManager, ICollection<NodeDescriptor> results)
        {
            // Check if the node falls within the range or the document if there isn't a range
            if (rangeMarker == null || rangeMarker.IsValid == false || rangeMarker.DocumentRange.Contains(node.GetDocumentRange()))
            {
                NodeDescriptor descriptor = GetDescriptorForNode(node);

                if (descriptor != null && descriptor.IsValid)
                {
                    results.Add(descriptor);

                    // We have matched this node, do not recurse down to its children
                    return node;
                }
            }

            // Recursively call through all the siblings and child nodes
            ITreeNode child = node.FirstChild;

            if (child == null)
            {
                return node;
            }

            // Loop through the siblings and resolve them
            while (true)
            {
                child = ResolveNodeDescriptorsInternal(child, rangeMarker, psiManager, results);

                if (psiManager != null && psiManager.IsCurrentTransactionFailed)
                {
                    return node;
                }

                if (child == null)
                {
                    return node;
                }

                ITreeNode nextSibling = child.NextSibling;

                if (nextSibling == null)
                {
                    return node;
                }

                child = nextSibling;
            }
        }

        /// <summary>
        /// Gets the alias type mappings.
        /// </summary>
        /// <value>
        /// The alias type mappings.
        /// </value>
        public static IEnumerable<AliasTypeMapping> AliasTypeMappings
        {
            get
            {
                // Check if the type mappings have been created
                if (_aliasTypeMappings == null)
                {
                    _aliasTypeMappings = GenerateMappings();
                }

                return _aliasTypeMappings;
            }
        }
    }
}